"""
A table is 'meta' knowledge about some 'data'. All rows
of the 'data' are the same size and the i-th column
is described by the i-th item in 'meta'.

This code assumes that each row one 'klass' value stored
in the last item on each row.

This code is part of KNIFE: cropping complexity, culling confusion, 
and cutting the crap since 2012
   
           .----------------._________________
           |=()=========---  \-----------     `\ 
      jgs  \_________________/^^^^^^^^^^^^^^^^^^

(c) 2012, Tim Menzies, tim@menzies.us, LGPL v3.0

Todo
----
Support multiple classes from any column.
"""

from lib import *
from thing import *
from about import *

def leaveOneOut(file):
  """
  For each row of data, build a table on the
  other rows. Retuns that row, the klass of that
  row, and the table generated from the other data.
  """
  table= Table().slurp(file)
  data = table.data
  for n,test in enumerate(data) :
    train = [row for m,row in enumerate(data) if m != n]
    yield test,test[-1],table.clone(train)

class Table(object):
  def __init__(self) :
    """
    'Data' stores rows of data. The distances between 
    rows is cached in 'dists'. And 'meta' stores what
    we know about each column.
    """
    self.data  = []
    self.dists = {}
    self.meta  = []

  def slurp(self,file):
    """
    Reads data from a csv file. Assumes the first line 
    has column names with '$' denoting numbers and '!'
    denoting class variables. Also, names can (optionally)
    be followed by '*N' which sets the weight of this
    column.
    """
    for row,cells in rows(file,-2):
      if self.meta:
        self.body(row,cells)
      else: # first line
        self.meta = self.headers(cells)
    return self

  def median(self,what=lambda x: x[-1]):
    "Returns the median of a set of values computed from all rows."
    return medianSorted([what(x) for x in self.data])

  def clone(self,data=[]):
    """
    Returns a new table with the same column names and types
    as the current table, but with diffent rows (read in from
    'data'.
    """
    t = Table()
    t.meta = [header.clone() for header in self.meta]
    for n,cells in enumerate(data):
      t.body(n,cells)
    return t

  def header(self,n,str) :
    "Peeks at a header name and returns the right column type."
    w    = float(str.split('*')[1]) if '*' in str else 1
    details = Num if '$' in str else Sym
    klassp = '!' in str
    return About(n,str,w,details,klassp)

  def headers(self,cells) :
    "Calls 'header()' for 'cells' (which is a list of text strings)."
    return ([self.header(col,x) for col,x in enumerate(cells)])

  def body(self, row,cells):
    """
    Imports the 'row'-th set of cells. Cells contents are
    converted to their right type by the 'keep' function.
    """
    klass = cells[-1]
    new = [self.meta[col].keep(klass,x,row) for col,x in enumerate(cells)]
    return self.data.append(new)

  def dist(self,r1,r2):
    """
    Return the distance between two rows of index 'r1' and 'r2'
    in data. For effeciecny reasons, results cached in 'dists'.
    """
    if r1 == r2 : return 0
    if r1 >  r2 : return self.dist(r2,r1)
    key = (r1,r2)
    if not self.dists.has_key(key): 
      self.dists[key] = self.dist1(self.data[r1],
                                   self.data[r2])
    return self.dists[key]

  def dist1(self,cells1,cells2) :
    """
    Primitive distance measure between 2 sets of cells.
    Implements Aha's 1991 distance measure: same things
    get distance '0', different symbols get distance '0'.
    If any missing values, then the max possible distance is
    returns. Returns a value 0..1.
    """
    n=total=0
    for header in self.meta:
      if header.klassp: continue
      n  += 1
      a   = cells1[header.pos]
      b   = cells2[header.pos]
      tmp = 1
      if a == b:
        if a != '?' : tmp=0
      else:
        if header.isNumeric():
          lo = header.all().min
          hi = header.all().max
          if lo == hi: tmp = 0
          else:
            half = (hi - lo)/2.0
            if a == '?' : a = (lo if b > half else hi)
            if b == '?' : b = (lo if a > half else hi)
            a   = (a - lo) / (hi - lo)
            b   = (b - lo) / (hi - lo)
            tmp = a - b
      total += header.weight*(tmp**2)
    return total**0.5/n**0.5  

  def nearest(self,cells1) :
    "Returns the row nearest 'cells1' (a list of cells)."
    least = 10**10
    for cells2 in self.data:
      tmp=self.dist1(cells1,cells2)
      if tmp < least :
        least = tmp
        out  = cells2
    return out

  def outmost(self,r1) :
    "Returns the row, and its distance, to data[r1]."
    most = -1
    for r2,row in enumerate(self.data):
      tmp=self.dist(r1,r2)
      if tmp > most:
        most = tmp
        out  = r2
    return (out,most)

  def outmost2(self) :
    "Returns the two most distant points in O(N^2) time."
    most = -1
    for r1,row in enumerate(self.data):
      for r2,row in enumerate(self.data):
        if r1 > r2:
          tmp=self.dist(r1,r2)
          if tmp > most:
                most = tmp
                west  = r1
                east  = r2
    return (west,east,most)

